#include "tcp_connection.hh"

#include "tcp_config.hh"
#include "tcp_segment.hh"

#include <iostream>

// Dummy implementation of a TCP connection

// For Lab 4, please replace with a real implementation that passes the
// automated checks run by `make check`.

template <typename... Targs>
void DUMMY_CODE(Targs &&.../* unused */) {}

using namespace std;

size_t TCPConnection::remaining_outbound_capacity() const { return _sender.stream_in().remaining_capacity(); }

size_t TCPConnection::bytes_in_flight() const { return _sender.bytes_in_flight(); }

size_t TCPConnection::unassembled_bytes() const { return _receiver.unassembled_bytes(); }

size_t TCPConnection::time_since_last_segment_received() const { return _time_since_last_recv; }

void TCPConnection::segment_received(const TCPSegment &seg) {
    _time_since_last_recv = 0;
    if (seg.header().rst) {
        _receiver.stream_out().set_error();
        _sender.stream_in().set_error();
        _linger_after_streams_finish = false;
        return;
    }
    _receiver.segment_received(seg);
    if (seg.header().ack) {
        _sender.ack_received(seg.header().ackno, seg.header().win);
    }
    if (seg.length_in_sequence_space() != 0) {
        _sender.fill_window();
        if (_sender.segments_out().empty()) {
            _sender.send_empty_segment();
        }
    }
    if (_receiver.ackno().has_value() && (seg.length_in_sequence_space() == 0) &&
        seg.header().seqno == _receiver.ackno().value() - 1) {
        _sender.send_empty_segment();
    }
    push_queue();
}

bool TCPConnection::active() const {
    if (_receiver.stream_out().error() || _sender.stream_in().error()) {
        return false;
    }
    if (!_receiver.stream_out().eof() || !_sender.stream_in().eof() || bytes_in_flight() != 0) {
        return true;
    }
    return _linger_after_streams_finish;
}

size_t TCPConnection::write(const string &data) {
    size_t len = _sender.stream_in().write(data);
    _sender.fill_window();
    push_queue();
    return len;
}

//! \param[in] ms_since_last_tick number of milliseconds since the last call to this method
void TCPConnection::tick(const size_t ms_since_last_tick) {
    if (_listening) {
        return;
    }
    _time_since_last_recv += ms_since_last_tick;
    _sender.tick(ms_since_last_tick);
    if (_sender.consecutive_retransmissions() > _cfg.MAX_RETX_ATTEMPTS) {
        send_rst();
        return;
    }
    _sender.fill_window();
    push_queue();

    // clean end
    bool preq1 = inbound_stream().eof();
    bool preq2 = _sender.stream_in().eof();
    bool preq3 = _sender.bytes_in_flight() == 0;

    // active close
    if (preq1 && preq2 && preq3 && _time_since_last_recv >= 10 * _cfg.rt_timeout) {
        _linger_after_streams_finish = false;
    }
}

void TCPConnection::end_input_stream() {
    _sender.stream_in().end_input();
    _sender.fill_window();
    push_queue();
}

void TCPConnection::connect() {
    _sender.fill_window();
    push_queue();
}

TCPConnection::~TCPConnection() {
    try {
        if (active()) {
            cerr << "Warning: Unclean shutdown of TCPConnection\n";

            send_rst();
        }
    } catch (const exception &e) {
        std::cerr << "Exception destructing TCP FSM: " << e.what() << std::endl;
    }
}

void TCPConnection::push_queue() {
    _listening = false;
    auto &q = _sender.segments_out();
    while (!q.empty()) {
        auto &seg = q.front();
        if (_receiver.ackno().has_value()) {
            seg.header().ack = true;
            seg.header().ackno = _receiver.ackno().value();
        }
        seg.header().win = _receiver.window_size();
        _segments_out.emplace(std::move(seg));
        q.pop();
    }
    // passive close
    if (inbound_stream().input_ended() && !_sender.stream_in().eof()) {
        _linger_after_streams_finish = false;
    }
}

void TCPConnection::send_rst() {
    TCPSegment rst_seg{};
    rst_seg.header().rst = true;

    _segments_out.emplace(std::move(rst_seg));
    _sender.stream_in().set_error();
    _receiver.stream_out().set_error();
    _linger_after_streams_finish = false;
}